/*
 * Copyright (c) 2011 Imre Fazekas.
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the name of the Brillien nor the names of its
 * terms and concepts may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.vii.brillien.ignition.transport.xmpp;

import com.vii.brillien.ignition.BrillienContext;
import com.vii.brillien.ignition.transport.BrillienCommunication;
import com.vii.brillien.ignition.transport.BrillienMediator;
import com.vii.brillien.kernel.BrillienException;
import com.vii.brillien.kernel.axiom.transport.Communication;
import com.vii.brillien.kernel.axiom.transport.CommunicationListener;
import com.vii.brillien.kernel.axiom.transport.Manager;
import com.vii.brillien.kernel.axiom.transport.MessageProcessor;
import com.vii.streamline.services.ThreadServices;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.filter.ThreadFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.RemoteRosterEntry;
import org.jivesoftware.smackx.RosterExchangeListener;
import org.jivesoftware.smackx.RosterExchangeManager;

import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

/**
 * Mediator call handling all XMPP-related operations.
 */
public class XmppMediator extends BrillienMediator<Message> implements Manager<XmppMediator> {

    /**
     * IP address of the XMPP server
     */
    protected String                        host;

    /**
     * Port of the XMPP server
     */
    protected int                           port;

    /**
     * Domain name of the XMPP server
     */
    protected String                        domain;

    /**
     * Name and password of the user for authentication while logging in.
     */
    protected String                        username, password, resource;

    /**
     * Blocking storage for the {@XMPPConnection} instance.
     */
    protected BlockingQueue<XMPPConnection> connQueue;

    /**
     * Roster instance retrieved from the Connection instance
     */
    protected Roster                        ros;

    /**
     * AccountManager instance defined by XMPP specification
     */
    protected AccountManager                am;

    /**
     * RosterExchangeManager used to exchange roster data
     */
    protected RosterExchangeManager         rosterExchangeManager;

    /**
     * {@XmppConnectionListener} instance to deal with occurring Connectivity problems.
     */
    protected XmppConnectionListener        listener;

    private Presence.Type                   presence;
    private Presence.Mode                   mod;

    {
        host = "localhost";
        port = 5222;
        username = "liaison" + this.getClass().getSimpleName();
        password = "liaison";
        resource = XmppServices.DEFAULT_RESOURCE_NAME;

        connQueue = new LinkedBlockingQueue<XMPPConnection>(1);

        listener = new XmppConnectionListener();
    }

    /**
     * Specialized RosterExchangeListener adding evety received Roster data as a new Contect
     */
    protected class XmppRosterExchangeListener implements RosterExchangeListener {
        @Override
        public void entriesReceived(String from, Iterator remoteRosterEntries) {
            BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Roster exchanged evemt from: " + from);
            while (remoteRosterEntries.hasNext()) {
                try {
                    // Get the received entry
                    RemoteRosterEntry remoteRosterEntry = (RemoteRosterEntry) remoteRosterEntries.next();

                    BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Exchanged remote entity:", remoteRosterEntry.getUser(), remoteRosterEntry.getName());

                    // Add the entry to the user2's roster
                    addContact(
                            remoteRosterEntry.getUser(),
                            remoteRosterEntry.getName(),
                            remoteRosterEntry.getGroupArrayNames()
                    );
                } catch (BrillienException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Specialized ConnectionListener managing connectivity-related events
     */
    protected class XmppConnectionListener implements ConnectionListener {
        @Override
        public void connectionClosed() {
            BrillienContext.basicLog(Level.WARNING, mediatorEntity() + ":: " + "Connection closed to" + host + ":" + port + "@" + domain + "/" + XmppMediator.this.username);
        }

        @Override
        public void connectionClosedOnError(Exception e) {
            BrillienContext.basicLog(Level.WARNING, mediatorEntity() + ":: " + "Connection closed on Error" + host + ":" + port + "@" + domain + "/" + XmppMediator.this.username, e.getMessage());
            connQueue.poll();
            try {
                createConnection();
            } catch (BrillienException be) {
                BrillienContext.exceptionLog( XmppMediator.class.getName(), "connectionClosedOnError", be );
            }
        }

        @Override
        public void reconnectingIn(int i) {
            BrillienContext.basicLog(Level.WARNING, mediatorEntity() + ":: " + "Reconnecting in " + i + " to " + host + ":" + port + "@" + domain + "/" + XmppMediator.this.username);
        }

        @Override
        public void reconnectionSuccessful() {
            setPresence( connQueue.peek(), presence, mod);
            BrillienContext.basicLog(Level.WARNING, mediatorEntity() + ":: " + "Reconnection was Successful to" + host + ":" + port + "@" + domain + "/" + XmppMediator.this.username);
        }

        @Override
        public void reconnectionFailed(Exception e) {
            BrillienContext.basicLog(Level.WARNING, mediatorEntity() + ":: " + "Reconnection Failed to" + host + ":" + port + "@" + domain + "/" + XmppMediator.this.username, e.getMessage());
        }
    }

    public XmppMediator() {
    }

    public XmppMediator(String host, int port, String domainName, String username, String password) {
        this(host, port, domainName, username, password, XmppServices.DEFAULT_RESOURCE_NAME );
    }

    public XmppMediator(String host, int port, String domain, String username, String password, String resource) {
        this.host = host;
        this.port = port;
        this.domain = domain;
        this.username = username;
        this.password = password;
        this.resource = resource;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getResource() {
        return resource;
    }

    public void setResource(String resource) {
        this.resource = resource;
    }

    protected void createConnection() throws BrillienException {
        for( int i=0; i<BrillienContext.RECONNECTION_ATTEMPT; ++i ){
            try {
                ConnectionConfiguration config =
                        domain == null
                                ? new ConnectionConfiguration(
                                host, port
                        )
                                : new ConnectionConfiguration(
                                host, port, domain
                        );
                config.setCompressionEnabled(true);
                config.setReconnectionAllowed(true);

                XMPPConnection conn = new XMPPConnection(config);

                conn.connect();

                conn.login(username.toLowerCase(), password.toLowerCase(), resource);

                BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Connected.");

                conn.addConnectionListener(listener);

                ros = conn.getRoster();

                BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Getting roster...");
                for (RosterEntry entry : ros.getEntries()) {
                    BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Friend: " + entry);
                }

                ros.setSubscriptionMode(Roster.SubscriptionMode.accept_all);

                rosterExchangeManager = new RosterExchangeManager( conn );
                rosterExchangeManager.addRosterListener(new XmppRosterExchangeListener());

                BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Getting account manager...");

                am = conn.getAccountManager();

                connQueue.offer(conn);

                return;
            } catch (XMPPException e) { }
            CountDownLatch latch = new CountDownLatch( 1 );
            try {
                latch.await( BrillienContext.RECONNECTION_TIMEOUT, TimeUnit.MILLISECONDS );
            } catch (InterruptedException e) { }
        }
        throw new BrillienException("Cannot get a connection");
    }

    protected XMPPConnection getConnection() throws BrillienException {
        XMPPConnection conn = null;
        try {
            for( int i=0; conn==null && i<BrillienContext.RECONNECTION_ATTEMPT; ++i ){
                BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Attempting to get a connection...", i );

                conn = connQueue.poll( BrillienContext.RECONNECTION_TIMEOUT, TimeUnit.MILLISECONDS );
            }
            if( conn == null )
                throw new BrillienException("Cannot get a connection");

            connQueue.offer( conn );
            return conn;
        } catch (InterruptedException e) {
            throw new BrillienException( e );
        }
    }

    /**
     * Set the presence type of the connected entity
     */
    public void setPresence(XMPPConnection conn, Presence.Type t, Presence.Mode mode) {
        BrillienContext.basicLog( Level.FINE, mediatorEntity() + ":: " + " presense has been set to " + t + " " + mode, t, mode );

        Presence presence = new Presence(this.presence = t);
        presence.setMode(this.mod = mode);
        conn.sendPacket(presence);
    }

    /**
     * Adds new RosterListener to listen roster events
     *
     * @return actual XmppMediator instance (this)
     */
    public XmppMediator addRosterListener(RosterListener listener) {
        ros.addRosterListener(listener);
        return this;
    }

    /**
     * Adds new PacketListener to listen a given packet type.
     *
     * @param packetType class instance of the type of the packet to be listened.
     * @param listener   listener instence
     * @param <T>        Packet type
     * @return actual XmppMediator instance (this)
     */
    public <T extends Packet> XmppMediator addPacketListener(Class<T> packetType, PacketListener listener) throws BrillienException {
        getConnection().addPacketListener(listener, new PacketTypeFilter(packetType));
        return this;
    }

    public void connect() throws BrillienException {
        BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Connecting to: " + host + " " + port + " " + domain);

        createConnection();

        startListening();
    }

    @Override
    public void disconnect() throws BrillienException {
        connQueue.peek().disconnect();

        BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "Disconnected.");
    }

    @Override
    public void startListening() throws BrillienException {
        try {
            BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + " started to listen...");

            addPacketListener(Message.class, new PacketListener() {
                @Override
                public void processPacket(Packet packet) {
                    try {
                        BrillienContext.basicLog( Level.FINE, mediatorEntity() + ":: " + "package received " + packet.toXML() );
                        Message m = (Message) packet;

                        for( CommunicationListener<Message> listener : getCommunicationListeners() ){
                            try{
                                listener.received( m );
                            }  catch(BrillienException e){
                                BrillienContext.exceptionLog( this.getClass().getName(), "startListening", e );
                            }
                        }

                        if( m.getBody() == null || !m.getBody().trim().startsWith( XmppServices.MESSAGE_RPC_PREFIX ) )
                            return;

                        XmppCommunication message = XmppCommunication.wrap( m );

                        for( MessageProcessor processor : getNewMessageProcessorInstances( message ) ){
                            ThreadServices.getStpe().execute( processor );
                        }
                    } catch (Exception e) {
                        BrillienContext.exceptionLog( this.getClass().getName(), "startListening", e );
                    }
                }
            });
        } catch (Exception e) {
            throw new BrillienException(e);
        }
    }


    @Override
    public XmppCommunication newCommunication() throws BrillienException {
        return new XmppCommunication();
    }

    protected String validateXmppRecipient( String to ){
        return to.contains("@")
                    ? to
                    : to + "@" + domain;
    }

    @Override
    public BrillienCommunication sendCommunication(long timeout, HashMap<String, Object> properties, BrillienCommunication comm) throws BrillienException {

        String message = getMessageToSend(comm);

        Message msg = new Message( validateXmppRecipient( comm.getRecipient().toLowerCase() ) );

        msg.setBody( null );
        msg.setBody(
            XmppServices.MESSAGE_RPC_PREFIX + XmppServices.MESSAGE_DELIMETER + message
        );
        msg.setPacketID( comm.getId() );
        msg.setThread( comm.getOriginalId() );

        PacketCollector collector = null;
        if( comm.getMessageType() == Communication.TYPE_GET ){
            ThreadFilter filter = new ThreadFilter( msg.getThread() );

            collector = getConnection().createPacketCollector( filter );
        }

        BrillienContext.basicLog( Level.FINE, mediatorEntity() + ":: " + "sending packet " + msg.toXML() );

        try{
            getConnection().sendPacket( msg );
        } catch (BrillienException e) {
            //BrillienContext.exceptionLog(XmppMediator.class.getName(), "sendCommunication", e);
            throw e; //new BrillienException(e);
        }

        if( comm.getMessageType() == Communication.TYPE_GET ){
            try{
                BrillienContext.basicLog( Level.FINE, mediatorEntity() + ":: " + "waiting for response for " + comm );

                Message result = (Message)collector.nextResult( timeout );

                collector.cancel();

                BrillienContext.basicLog( Level.FINE, mediatorEntity() + ":: " + "response received " + (result == null ? "null" : result.toXML()) + " to " + comm );

                if (result == null) {
                    throw new BrillienException( "No response from server for communication: " + comm );
                }
                else if (result.getError() != null) {
                    throw new BrillienException( result.getError().getCode(), result.getError().getMessage() );
                }

                return XmppCommunication.wrap( result );
            } catch (BrillienException e) {
                //BrillienContext.exceptionLog(XmppMediator.class.getName(), "sendCommunication", e);
                throw e; //new BrillienException(e);
            }
        }

        return null;
    }

    @Override
    public String mediatorEntity() {
        return username;
    }


    /**
     * Adds a new contact to the contact list of the user logged in with this mediator
     *
     * @throws XMPPException in case of unsuccessful operation
     */
    public void addContact(String user) throws BrillienException {
        addContact(user, user);
    }

    /**
     * Adds a new contact to the contact list of the user logged in with this mediator
     *
     * @throws XMPPException in case of unsuccessful operation
     */
    public void addContact(String user, String name) throws BrillienException {
        addContact(user, name, null);
    }

    /**
     * Adds a new contact to the contact list of the user logged in with this mediator
     *
     * @throws XMPPException in case of unsuccessful operation
     */
    public void addContact(String user, String name, String[] groups) throws BrillienException {
        BrillienContext.basicLog(Level.FINE, mediatorEntity() + ":: " + "is adding a contact:: ", username, user, name);

        if (!user.contains("@"))
            user += "@" + this.domain;

        if (!ros.contains(user) && !user.startsWith(this.username + "@")) {
            try {
                ros.createEntry(user, name, groups);
            } catch (XMPPException e) {
                throw new BrillienException( e );
            }
            rosterExchangeManager.send(ros, user);
        }
    }

    /**
     * Creates a user with a given username and password
     * @throws XMPPException in case of unsuccessful operation
     */
    public void createUser( String user, String pass ) throws BrillienException {
        if( !am.supportsAccountCreation() )
            throw new BrillienException("Account creation is not supported...");

        BrillienContext.basicLog( Level.FINE, mediatorEntity() + ":: " + " is creating an entity: " + user );

        try{
            am.createAccount( user, pass );
        } catch( XMPPException xe ){
            if( xe.getXMPPError().getCode() == 409 ){ }
            else throw new BrillienException( xe );
        }
    }

    @Override
    public void createAccountFor(XmppMediator xmppMediator) throws BrillienException {
        String username = xmppMediator.getUsername();
        String password = xmppMediator.getPassword();

        createUser(username, password);

        addContact( username );
    }


}
